//
// Created by haoy on 2017/6/27.
//

#include "rtc_recorder.h"

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <ios>
#include <list>
#include <map>
#include <cmath>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <sstream>
#include <string>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <random>


#include "xcutil.h"
#include "xm_log.h"
#include "xmixer.h"
#include "xrtc_endpoint.h"

// FAKE_RECORDING is for debugging purpose
//#define FAKE_RECODING

int RTC_RECORDER_TLV_REUSED = 100;

class rtc_recorder {
public:
    class segment {
    public:
        std::string scene;
        std::string source;
        std::string rtc;
        bool audio = false;
        bool video = false;
        uint64_t begin_ts = 0;
        uint64_t end_ts = (uint64_t)(-1);
        bool live = false;
    };

    class source {
    public:
        std::map<std::string, segment> segments;
    };

    class scene {
    public:
        std::string type;
        int priority;
        std::string ctx;
        uint64_t timeout_ms;
        std::map<std::string, source> sources;
        uint64_t expire_at_ms;
        int segment_counter = 1;
        std::string date;
        std::string time;
        std::string sub_dir;
    };

    class scene_and_source {
    public:
        std::string scene;
        std::string source;
    };

    std::map<std::string, scene> scenes;
    std::multimap<std::string, scene_and_source> sources_by_internal_source;
    std::string job_path;
    std::string tlv_path;
    int scene_counter = 1;
};

static bool _is_scene_live(const std::string& scene_name, rtc_recorder::scene &scene) {
    auto source_it = scene.sources.begin();
    while (source_it != scene.sources.end()) {
        auto& source = source_it->second;
        auto segment_it = source.segments.begin();
        while (segment_it != source.segments.end()) {
            auto& segment = segment_it->second;
            if (segment.live) {
                dbgi("segment %s/%s/%s alive", scene_name.c_str(), source_it->first.c_str(), segment_it->first.c_str());
                break;
            } else {
                dbgi("segment %s/%s/%s dead", scene_name.c_str(), source_it->first.c_str(), segment_it->first.c_str());
            }
            ++segment_it;
        }
        if (segment_it != source.segments.end()) {
            break;
        }
        ++source_it;
    }
    return source_it != scene.sources.end();
}

static int _mkdir_tree(const char *path) {
    char tmp[1024] = {0,};
    char *p = nullptr;
    snprintf(tmp, sizeof(tmp), "%s", path);
    size_t len = strlen(tmp);
    if (tmp[len - 1] == '/') {
        tmp[len - 1] = 0;
    }
    for (p = tmp + 1; *p; p++) {
        if(*p == '/') {
            *p = 0;
            mkdir(tmp, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
            *p = '/';
        }
    }
    mkdir(tmp, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
    return 0;
}

static int _close_scene(rtc_recorder_t rec, const std::string id) {
    auto scene_it = rec->scenes.find(id);
    if (scene_it == rec->scenes.end()) {
        return -100;
    }
    int segment_count = 0;
    auto& scene = scene_it->second;
    uint64_t now = (uint64_t)get_timestamp_ms();
    rapidjson::StringBuffer ctx_buf;
    rapidjson::Writer<rapidjson::StringBuffer> ctx_writer(ctx_buf);
    ctx_writer.String(scene.ctx);

    std::ostringstream w;
    w << "{" << std::endl;
    w << "    \"scene\":\"" << id << "\"," << std::endl;
    w << "    \"date\":\"" << scene.date << "\"," << std::endl;
    w << "    \"time\":\"" << scene.time << "\"," << std::endl;
    w << "    \"type\":\"" << scene.type << "\"," <<std::endl;
    w << "    \"priority\":" << scene.priority << "," << std::endl;
    w << "    \"ctx\":" << std::string(ctx_buf.GetString(), ctx_buf.GetSize()) << "," << std::endl;
    w << "    \"sub_dir\":\"" << scene.sub_dir << "\"," << std::endl;
    w << "    \"sources\":[" << std::endl;
    auto source_it = scene.sources.begin();
    while (source_it != scene.sources.end()) {
        auto& source = source_it->second;
        w << "        {" << std::endl;
        w << "          \"name\":\"" << source_it->first << "\"," << std::endl;
        w << "          \"segments\": [" << std::endl;
        auto segment_it = source.segments.begin();
        while (segment_it != source.segments.end()) {
            auto& segment = segment_it->second;
            if (segment.live) {
                segment.end_ts = now;
                segment.live = false;
            }
            w << "              {\"rtc\":\"" << segment.rtc << "\",\"audio\":" << (segment.audio ? "true" : "false") << ",\"video\":" << (segment.video ? "true" : "false") << ",\"from\":" << segment.begin_ts << ",\"to\":" << segment.end_ts << "}";
            auto next_segment_it = segment_it;
            next_segment_it++;
            if (next_segment_it != source.segments.end()) {
                w << ",";
            }
            w << std::endl;
            ++segment_it;
            ++segment_count;
        }
        w << "          ]" << std::endl; // end of segments
        w << "        }";
        auto next_source_it = source_it;
        next_source_it++;
        if (next_source_it != scene.sources.end()) {
            w << ",";
        }
        w << std::endl;
        ++source_it;
    }
    w << "    ]" << std::endl; // end of sources
    w << "}" << std::endl;

//    if (segment_count == 0) {
//        dbge("recorder close scene %s failed: no segment", id.c_str());
//        rec->scenes.erase(scene_it);
//        return -110;
//    }
    std::ostringstream fn;
    fn << rec->job_path << "/" << scene.date;
    if (!scene.sub_dir.empty()) {
        fn << "/" << scene.sub_dir;
    }
    _mkdir_tree(fn.str().c_str());
    fn << "/" << id << ".job.tmp";
    std::ofstream fw(fn.str().c_str(), std::ios::out | std::ios::trunc);
    if (!fw.is_open()) {
        dbge("close recorder scene: can't open record job file %s", fn.str().c_str());
        rec->scenes.erase(scene_it);
        return -120;
    }
    std::ostringstream fn2;
    fn2 << rec->job_path << "/" << scene.date;
    if (!scene.sub_dir.empty()) {
        fn2 << "/" << scene.sub_dir;
    }
    _mkdir_tree(fn2.str().c_str());
    fn2 << "/" << id << ".job";
    int err = rename(fn.str().c_str(),fn2.str().c_str());
    if (err != 0) {
        dbge("close recorder scene: can't rename record job file from %s to %s", fn.str().c_str(), fn2.str().c_str());
        rec->scenes.erase(scene_it);
        fw.close();
        return -130;
    }

    rec->scenes.erase(scene_it);
    dbgi("close recorder scene: created job file %s", fn.str().c_str());
    fw << w.str();
    fw.close();
    return 0;
};

rtc_recorder_t rtc_recorder_new(const std::string& job_path, const std::string& tlv_path) {
    dbgv("creating recorder");
    if (job_path.empty()) {
        dbge("failed to create recorder with empty job path");
        return nullptr;
    }
    auto recorder = new rtc_recorder();
    recorder->job_path = job_path;
    recorder->tlv_path = tlv_path;
    return recorder;
}

void rtc_recorder_delete(rtc_recorder_t rec) {
    dbgv("destroying recorder");
    for (auto scene_it : rec->scenes) {
        auto& scene_id = scene_it.first;
        _close_scene(rec, scene_id);
    }
}

void rtc_recorder_on_internal_source_closed(rtc_recorder_t rec, const std::string& internal_source_name) {
    std::list<std::string> affected_scene_ids;
    auto source_id_range = rec->sources_by_internal_source.equal_range(internal_source_name);
    auto source_id_it = source_id_range.first;
    // for all source related to this rtc, search for any segment still alive, close it
    while (source_id_it != source_id_range.second) {
        auto& source_id = source_id_it->second;
        auto scene_it = rec->scenes.find(source_id.scene);
        if (scene_it == rec->scenes.end()) {
            ++source_id_it;
            continue;
        }
        auto& scene = scene_it->second;
        auto source_it = scene.sources.find(source_id.source);
        if (source_it == scene.sources.end()) {
            ++source_id_it;
            continue;
        }
        auto& source = source_it->second;
        auto segment_it = source.segments.begin();
        while (segment_it != source.segments.end()) {
            auto& segment = segment_it->second;
            if (segment.live) {
                dbgi("recorder closed segment %s/%s/%s", scene_it->first.c_str(), source_it->first.c_str(), segment_it->first.c_str());
                segment.end_ts = (uint64_t)get_timestamp_ms();
                segment.live = false;
                affected_scene_ids.emplace_back(source_id.scene);
            }
            ++segment_it;
        }
        ++source_id_it;
    }
    rec->sources_by_internal_source.erase(internal_source_name);

    // search for scene has no live segment, and start expiry timer
    for (auto& id : affected_scene_ids) {
        auto& scene = rec->scenes[id];
        if (!_is_scene_live(id, scene)) {
            dbgi("recorder scene %s will expire in %lu seconds", id.c_str(), scene.timeout_ms/1000);
            scene.expire_at_ms = get_timestamp_ms() + scene.timeout_ms;
        } else {
            dbgi("recorder scene %s still live", id.c_str());
        }
    }
}

void rtc_recorder_check_timeout(rtc_recorder_t rec, uint64_t now) {
    std::list<std::string> expired;
    for (auto scene_it : rec->scenes) {
        auto& scene = scene_it.second;
        if (scene.expire_at_ms != 0 && scene.expire_at_ms <= now) {
            expired.emplace_back(scene_it.first);
        }
    }
    for (auto& e : expired) {
        dbgw("recorder scene %s timeout", e.c_str());
        _close_scene(rec, e);
    }
}

static
std::string generate_id(const std::string& prefix, int& seq, int seq_width){
    static std::string tab = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static std::random_device r;
    std::ostringstream w;
    auto now = std::chrono::system_clock::now();
    time_t tt = std::chrono::system_clock::to_time_t(now);
    tm local_tm = *localtime(&tt);
    w << prefix << "-"
      << std::setw(4) << std::setfill('0') << local_tm.tm_year+1900
      << std::setw(2) << std::setfill('0') << local_tm.tm_mon+1
      << std::setw(2) << std::setfill('0') << local_tm.tm_mday
      << std::setw(2) << std::setfill('0') << local_tm.tm_hour
      << std::setw(2) << std::setfill('0') << local_tm.tm_min
      << std::setw(2) << std::setfill('0') << local_tm.tm_sec
      << "-"
      << std::setw(seq_width) << std::setfill('0') << seq++;
    if (seq >= std::pow(10, seq_width)) {
        seq = 1;
    }
//    w << tab.substr(r()%52,1);
//    w << tab.substr(r()%52,1);
//    w << tab.substr(r()%52,1);

    return w.str();
}

int rtc_recorder_new_scene(rtc_recorder_t rec, std::string &name, int priority, const std::string &output_type, const std::string &ctx, int timeout_ms, const std::string& sub_dir) {
    do {
        name = generate_id("scn", rec->scene_counter, 4);
    } while(rec->scenes.count(name) != 0);


    rtc_recorder::scene scene;
    scene.priority = priority;
    scene.type = output_type;
    scene.ctx = ctx;
    scene.timeout_ms = timeout_ms;
    scene.sub_dir = sub_dir;
    std::ostringstream w;
    auto now = std::chrono::system_clock::now();
    auto epoch_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
    scene.expire_at_ms = epoch_ms + timeout_ms;
    auto t = std::chrono::system_clock::to_time_t(now);
    auto lt = std::localtime(&t);
    // gcc 4.8.5 have no put_time
    //w << std::put_time(lt, "%Y%m%d");
    w << std::setw(4) << std::setfill('0') << lt->tm_year+1900;
    w << std::setw(2) << std::setfill('0') << lt->tm_mon+1;
    w << std::setw(2) << std::setfill('0') << lt->tm_mday;
    scene.date = w.str();
    w.str("");
    //w << std::put_time(lt, "%H%M%S");
    w << std::setw(2) << std::setfill('0') << lt->tm_hour;
    w << std::setw(2) << std::setfill('0') << lt->tm_min;
    w << std::setw(2) << std::setfill('0') << lt->tm_sec;
    scene.time = w.str();

    rec->scenes.emplace(name, scene);

    dbgi("recorder new scene %s created", name.c_str());
    return 0;
}

int rtc_recorder_delete_scene(rtc_recorder_t rec, const std::string &name) {
    int err = _close_scene(rec, name);
    if (err != 0) {
        dbge("recorder delete scene %s err %d", name.c_str(), err);
        return err;
    }
    dbgi("recorder deleted scene %s", name.c_str());
    return 0;
}

int rtc_recorder_open_segment(rtc_recorder_t rec, const std::string &scene_name, const std::string &source_name, std::string &segment_name,
                              const std::string& tlv_pathname, const std::string& internal_source_name, bool audio, bool video) {
    auto scene_it = rec->scenes.find(scene_name);
    if (scene_it == rec->scenes.end()) {
        dbge("recorder start segment %s/%s failed: scene not found", scene_name.c_str(), source_name.c_str());
        return -200;
    }

    if (tlv_pathname.find(rec->tlv_path) == std::string::npos) {
        dbge("recorder start segment %s/%s failed: wrong tlv pathname [%s]", scene_name.c_str(), source_name.c_str(), tlv_pathname.c_str());
        return -201;
    }
    auto tlv_rel_pathname = tlv_pathname.substr(rec->tlv_path.length() + 1);
    auto& scene = scene_it->second;
    auto source_it = scene.sources.find(source_name);
    if (source_it == scene.sources.end()) {
        rtc_recorder::source source;
        auto inserted = scene.sources.emplace(source_name, source);
        source_it = inserted.first;
    }
    rtc_recorder::scene_and_source ss;
    ss.scene = scene_name;
    ss.source = source_name;
    rec->sources_by_internal_source.emplace(internal_source_name, ss);

    auto& source = source_it->second;
    do {
        segment_name = generate_id("sgm", scene.segment_counter, 2);
    } while(source.segments.count(segment_name) != 0);
    rtc_recorder::segment segment;
    segment.scene = scene_name;
    segment.source = source_name;
    segment.audio = audio;
    segment.video = video;
    segment.begin_ts = (uint64_t)get_timestamp_ms();
    segment.end_ts = (uint64_t)(-1);
    segment.live = true;
    segment.rtc = tlv_rel_pathname;
    scene.expire_at_ms = 0;
    source.segments.emplace(segment_name, segment);
//    if (video) {
//        rtc_endpoint_req_pli(rtc);
//    }
    dbgi("recorder opened segment %s/%s/%s", scene_name.c_str(), source_name.c_str(), segment_name.c_str());
    return 0;
}

int rtc_recorder_close_segment(rtc_recorder_t rec, const std::string &scene_name, const std::string &source_name,
                               const std::string &segment_name) {
    auto scene_it = rec->scenes.find(scene_name);
    if (scene_it == rec->scenes.end()) {
        dbgi("recorder close segment %s/%s/%s failed: scene not found", scene_name.c_str(), source_name.c_str(), segment_name.c_str());
        return -300;
    }
    auto& scene = scene_it->second;
    auto source_it = scene.sources.find(source_name);
    if (source_it == scene.sources.end()) {
        dbgi("recorder close segment %s/%s/%s failed: source not found", scene_name.c_str(), source_name.c_str(), segment_name.c_str());
        return -310;
    }
    auto& source = source_it->second;
    auto segment_it = source.segments.find(segment_name);
    if (segment_it == source.segments.end()) {
        dbgi("recorder close segment %s/%s/%s failed: segment not found", scene_name.c_str(), source_name.c_str(), segment_name.c_str());
        return -320;
    }
    auto& segment = segment_it->second;
    segment.live = false;
    segment.end_ts = (uint64_t)get_timestamp_ms();

    if (_is_scene_live(scene_name, scene)) {
        dbgi("recorder scene %s still alive", scene_name.c_str());
    } else {
        scene.expire_at_ms = get_timestamp_ms() + scene.timeout_ms;
        dbgi("recorder scene %s will expire in %lu seconds", scene_name.c_str(), scene.timeout_ms/1000);
    }
    dbgi("recorder closed segment %s/%s/%s", scene_name.c_str(), source_name.c_str(), segment_name.c_str());
    return 0;
}

int rtc_recorder_get_tlv_filename(rtc_recorder_t rec, const std::string &scene_name,
                                  const std::string& internal_source_name,
                                  std::string& tlv_filename) {
    auto scene_it = rec->scenes.find(scene_name);
    if (scene_it == rec->scenes.end()) {
        dbge("recorder prepare tlv for scene %s failed: scene not found", scene_name.c_str());
        return -200;
    }
    auto scene = scene_it->second;
    int index = 0;

    std::ostringstream path;
    path << rec->tlv_path << "/" << scene.date;
    mkdir_tree(path.str().c_str());

    while (true) {
        std::ostringstream rel_pathname;
        rel_pathname << scene.date << "/" << internal_source_name << "-" << index << ".tlv";
        std::ostringstream abs_pathname;
        abs_pathname << rec->tlv_path << "/" << rel_pathname.str();
        struct stat s;
        if (stat(abs_pathname.str().c_str(), &s) == 0) {
            index++;
            continue;
        }
        tlv_filename = abs_pathname.str();
        break;
    }
    return 0;
}

int rtc_recorder_update_ctx(rtc_recorder_t rec, const std::string &scene_name, const std::string &ctx) {
    auto scene_it = rec->scenes.find(scene_name);
    if (scene_it == rec->scenes.end()) {
        dbge("recorder update ctx for scene %s failed: scene not found", scene_name.c_str());
        return -200;
    }
    auto scene = scene_it->second;
    scene.ctx = ctx;
    return 0;
}

